package cn.blnp.net.flowable.api.manager;

import cn.hutool.core.date.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.api.management.TableMetaData;
import org.flowable.common.engine.api.management.TablePage;
import org.flowable.engine.*;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.impl.persistence.entity.HistoricProcessInstanceEntity;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.job.api.*;
import org.flowable.job.service.impl.persistence.entity.JobEntity;
import org.flowable.task.api.Task;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

/**
 * <h3>管理服务API案例演示</h3>
 *
 * @author <a href="mailto:blnp.yibin@qq.com">lyb</a>
 * @version 1.0
 * @since 2025/4/29 17:41
 */
@Slf4j
@SpringBootTest
public class ManagerApiTest {
    
    @Resource
    private ManagementService managementService;
    @Resource
    private RepositoryService repositoryService;
    @Resource
    private HistoryService historyService;
    @Resource
    private TaskService taskService;
    @Resource
    private RuntimeService runtimeService;
    
    /**
     *  <p>
     *      ManagementService接口提供了一系列数据库管理方法，包括数据表与表元数据的查询方法、数据库更新方法等：
     *  </p>
     *  <hr/>
     *  <ul>
     *      <li>
     *          <b>getTableCount()：</b>通过getTableCount()方法可以查询全部数据表的数据量，该方法返回一个Map，其key是表的名称，value是表的数据量，根据表名可以从Map中获取该表的数据量。
     *      </li>
     *      <li>
     *          <b>getTableName(Class<?> entityClass)：</b>通过getTableName()方法可以获取一个实体类对应的表名。
     *      </li>
     *      <li>
     *          <b>getTableMetaData(String tableName)：</b>通过getTableMetaData()方法可以获取数据表元数据信息，返回结果TableMetaData中包括表名、字段名称列表、字段类型列表等在内的基础信息。
     *      </li>
     *      <li>
     *          <b>createTablePageQuery()：</b>通过createTablePageQuery()方法可以创建数据表查询对象，可通过该对象的tableName()方法指定查询的数据表，通过orderAsc()、orderDesc()
     *          方法指定排序规则，通过listPage()获取分页查询结果。
     *      </li>
     *      <li>
     *          <b>databaseSchemaUpgrade(Connection connection, String catalog, String schema)：</b>通过databaseSchemaUpgrade()方法可以更新数据库schema，如创建表、删除表等。
     *      </li>
     *  </ul>
     */
    
    /**
     * <p><b>用途:根据API查询数据库表信息<b></p>
     * @author <a href="mailto:blnp.yibin@qq.com">lyb</a>
     * @since 17:46 2025/4/29
     * @params [] 
     * @param  
     * @return void
    **/
    @Test
    public void test() {
        //获取数据表的记录数量
        Map<String, Long> tableCount = managementService.getTableCount();
        for (Map.Entry<String, Long> entry : tableCount.entrySet()) {
            log.warn("表{}的记录数为{}", entry.getKey(), entry.getValue());
        }
        //获取数据表的名称及元数据
        String tableName = managementService.getTableName(HistoricProcessInstanceEntity.class);
        log.warn("HistoricProcessInstanceEntity对应的表名为：{}", tableName);
        TableMetaData tableMetaData = managementService.getTableMetaData(tableName);
        List<String> columnNames = tableMetaData.getColumnNames();
        log.warn("字段分别为：" + String.join(",", columnNames));
        //获取表的分页查询对象
        TablePage tablePage = managementService.createTablePageQuery()
                .tableName(tableName).orderAsc("START_TIME_").listPage(1, 10);
        log.warn("{}的记录数为{}", tableName, tablePage.getTotal());
    }

    /**
     *  <h3>异步任务管理功能</h3>
     *  <hr/>
     *  <p>
     *      Flowable中的定时器中间事件、定时器边界事件等都会产生异步任务。Flowable提供了6个用于存储异步任务的表。运行时异步任务存储在ACT_RU_JOB表,定时器任务存储在ACT_RU_TIMER_JOB表中，
     *   定时中断任务存储在ACT_RU_SUSPENDED_JOB表中，不可执行任务存储在ACT_RU_DEADLETTER_JOB表中，异步历史任务存储在ACT_RU_HISTORY_JOB表中，外部任务存储在ACT_RU_EXTERNAL_JOB表中。
     *  </p>
     *  <p>
     *      以定时器中间事件为例，异步任务产生后会被写入ACT_RU_TIMER_JOB表。任务达到触发条件后，会写入ACT_RU_JOB表中执行。如果该任务执行异常，可以重新执行。当重试次数大于设定的最大重试次数时，
     *   任务会被写入ACT_RU_DEADLETTER_JOB表。如果在任务等待过程中调用了中断流程实例的方法，异步任务将会被写入ACT_RU_SUSPENDED_JOB表。
     *  </p>
     **/


    /**
     * <p><b>用途:异步任务演示案例<b></p><br/>
     * <img src="https://free4.yunpng.top/2025/04/30/681187235d333.png" />
     * @author <a href="mailto:blnp.yibin@qq.com">lyb</a>
     * @since 10:03 2025/4/30
     * @params []
     * @param
     * @return void
    **/
    @Test
    public void test1() throws InterruptedException {
        /**
         *  常用API介绍：
         **/
        //获取异步任务查询对象
        managementService.createJobQuery();

        //获取外部工作任务查询对象
        //ExternalWorkerJobQuery createExternalWorkerJobQuery();

        //获取定时器任务查询对象
        //TimerJobQuery createTimerJobQuery();

        //获取挂起任务查询对象
        //SuspendedJobQuery createSuspendedJobQuery();

        //获取不可执行任务查询对象
        //DeadLetterJobQuery createDeadLetterJobQuery();

        //获取异步历史任务查询对象
        //HistoryJobQuery createHistoryJobQuery();

        //获取关联的ID查询job
        //Job findJobByCorrelationId(String jobCorrelationId);

        //强制执行任务
        //void executeJob(String jobId);

        //强制执行历史任务
        //void executeHistoryJob(String historyJobId);

        //查询历史任务JSON格式数据
        //String getHistoryJobHistoryJson(String historyJobId);

        //将定时器任务移入可执行任务
        //Job moveTimerToExecutableJob(String jobId);

        //将任务移入不可执行任务
        //Job moveJobToDeadLetterJob(String jobId);

        //将不可执行任务重新加入可执行任务
        //Job moveDeadLetterJobToExecutableJob(String jobId, int retries);

        //将不可执行任务加入历史任务
        //HistoryJob moveDeadLetterJobToHistoryJob(String jobId, int retries);

        //批量将不可执行任务加入可执行任务列表或历史任务列表
        //void bulkMoveDeadLetterJobs(Collection<String> jobIds, int retries);

        //批量将不可执行任务加入可执行历史任务列表
        //void bulkMoveDeadLetterJobsToHistoryJobs(Collection<String> jobIds, int retries);

        //将挂起任务重新加入可执行任务
        //Job moveSuspendedJobToExecutableJob(String jobId);

        //删除异步任务
        //void deleteJob(String jobId);

        //删除定时器任务
        //void deleteTimerJob(String jobId);

        //删除挂起的任务
        //void deleteSuspendedJob(String jobId);

        //删除不可执行的任务
        //void deleteDeadLetterJob(String jobId);

        //删除外部工作任务
        //void deleteExternalWorkerJob(String jobId);

        //删除历史任务
        //void deleteHistoryJob(String jobId);

        //设置异步任务剩余重试次数
        //void setJobRetries(String jobId, int retries);

        //设置定时器任务剩余重试次数
        //void setTimerJobRetries(String jobId, int retries);

        //重新设置定时器任务的执行日期
        //Job rescheduleTimeDateJob(String jobId, String timeDate);

        //通过时间段重新设置定时器任务的执行日期
        //Job rescheduleTimeDurationJob(String jobId, String timeDuration);

        //重新设置定时器的循环执行信息
        //Job rescheduleTimeCycleJob(String jobId, String timeCycle);

        //上述三个方式的合并，不过一次只能使用其中一种方式
        //Job rescheduleTimerJob(String jobId, String timeDate, String timeDuration, String timeCycle, String endDate, String calendarName);

        //获取异步任务异常栈
        //String getJobExceptionStacktrace(String jobId);

        //获取定时器任务异常栈
        //String getTimerJobExceptionStacktrace(String jobId);

        //获取挂起任务异常栈
        //String getSuspendedJobExceptionStacktrace(String jobId);

        //获取不可执行的任务异常栈
        //String getDeadLetterJobExceptionStacktrace(String jobId);

        //获取外部工作任务的详细异常信息
        //String getExternalWorkerJobErrorDetails(String jobId);

        /**
         *  特别声明：
         *      ManagementService接口提供6个后缀为Query()的方法，分别用于查询运行任务表、外部工作任务表、定时器任务表、挂起任务表、不可执行任务表及异步历史任务中的任务记录。
         *      需要注意的是，Flowable的任务表之间的数据可以相互转移，如ACT_RU_JOB、ACT_RU_TIMER_JOB、ACT_RU_ SUSPENDED_JOB和ACT_RU_ DEADLETTER_JOB中存储着不同状态的任务，
         *  当任务状态需要改变时，可以调用moveTimerToExecutableJob()、moveJobToDeadLetterJob()和moveDeadLetterJobToExecutableJob()方法转移任务。如下案例演示：
         **/
        Deployment deploy = repositoryService.createDeployment()
                //流程定义名称
                .name("异步任务案例")
                .addClasspathResource("flow/asyncDemo.bpmn20.xml")
                .deploy();
        //再次查询流程定义
        ProcessDefinition procDef = repositoryService
                .createProcessDefinitionQuery()
                .deploymentId(deploy.getId()).singleResult();
        log.info("流程定义ID为：{}，流程名称为：{}，版本号：{}", procDef.getId(),
                procDef.getName(), procDef.getVersion());
        //发起一个流程实例
        ProcessInstance procInst = runtimeService.startProcessInstanceById(procDef.getId());
        log.info("流程实例ID为：{}", procInst.getId());
        //查询第一个任务
        Task firstTask = taskService.createTaskQuery()
                .processInstanceId(procInst.getId())
                .singleResult();
        //完成第一个任务
        taskService.complete(firstTask.getId());
        //查询流程实例ID对应的job任务列表
        List<Job> timerJobs = managementService.createTimerJobQuery()
                .processInstanceId(procInst.getId())
                .list();
        for (Job job : timerJobs) {
            log.info("定时器任务 {} 的类型为 {}，执行时间为 {}", job.getId(),job.getJobType(), DateUtil.formatDateTime(job.getDuedate()));
            String tableName = managementService.getTableName(JobEntity.class);
            log.info("移动前 {} 表中存储了 {} 条记录", tableName, managementService.getTableCount().get(tableName));
            Job movJob = managementService.moveTimerToExecutableJob(job.getId());
            tableName = managementService.getTableName(JobEntity.class);
            log.info("移动后 {} 表中存储了 {} 条记录", tableName, managementService.getTableCount().get(tableName));
            log.info("定时器任务 {} 已被移动到可执行任务列表中", movJob.getId());

            //流程设计时指定的是需要10分钟后才会执行，若上述移动操作后Job没有触发执行可以通过调用executeJob()方法立即执行
            //log.info("立即执行定时器任务{}", job.getId());
            //managementService.executeJob(job.getId());
        }
        //查询当前流程任务信息
        historyService.createHistoricTaskInstanceQuery()
                .processInstanceId(procInst.getId())
                .list()
                .forEach(task -> {
                    log.info("任务ID为：{}，任务名称为：{}，办理时间为：{}",
                            task.getId(),
                            task.getName(),
                            DateUtil.formatDateTime(task.getEndTime()));
                });

        /**
         *  演示环境：
         *  2025-04-30 11:09:09.952  INFO 50880 --- [           main] c.b.n.f.api.manager.ManagerApiTest       : 流程定义ID为：asyncDemo:12:7c3af9bd-2570-11f0-b9e8-50ebf6ec8f6d，流程名称为：asyncDemo，版本号：12
         * 2025-04-30 11:09:09.994  INFO 50880 --- [           main] c.b.n.f.api.manager.ManagerApiTest       : 流程实例ID为：7c3f3f7e-2570-11f0-b9e8-50ebf6ec8f6d
         * 2025-04-30 11:09:10.091  INFO 50880 --- [           main] c.b.n.f.api.manager.ManagerApiTest       : 定时器任务 7c4ad847-2570-11f0-b9e8-50ebf6ec8f6d 的类型为 timer，执行时间为 2025-04-30 11:19:10
         * 2025-04-30 11:09:10.131  INFO 50880 --- [           main] c.b.n.f.api.manager.ManagerApiTest       : 移动前 ACT_RU_JOB 表中存储了 0 条记录
         * 2025-04-30 11:09:10.171  INFO 50880 --- [           main] c.b.n.f.api.manager.ManagerApiTest       : 移动后 ACT_RU_JOB 表中存储了 1 条记录
         * 2025-04-30 11:09:10.171  INFO 50880 --- [           main] c.b.n.f.api.manager.ManagerApiTest       : 定时器任务 7c4ad847-2570-11f0-b9e8-50ebf6ec8f6d 已被移动到可执行任务列表中
         * 2025-04-30 11:09:10.178  INFO 50880 --- [           main] c.b.n.f.api.manager.ManagerApiTest       : 任务ID为：7c42c1f3-2570-11f0-b9e8-50ebf6ec8f6d，任务名称为：申请，办理时间为：2025-04-30 11:09:10
         * 2025-04-30 11:09:10.179  INFO 50880 --- [           main] c.b.n.f.api.manager.ManagerApiTest       : 任务ID为：7c5d9cfa-2570-11f0-b9e8-50ebf6ec8f6d，任务名称为：复审，办理时间为：null
         **/
    }
}
